/*
 * Copyright (c) 2008, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.servlet;

import static jakarta.servlet.DispatcherType.REQUEST;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.http.Note;
import org.glassfish.grizzly.http.server.AfterServiceListener;
import org.glassfish.grizzly.http.server.HttpHandler;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.grizzly.http.server.Response;
import org.glassfish.grizzly.http.server.SessionManager;
import org.glassfish.grizzly.http.server.util.ClassLoaderUtil;
import org.glassfish.grizzly.http.server.util.DispatcherHelper;
import org.glassfish.grizzly.http.server.util.Globals;
import org.glassfish.grizzly.http.server.util.HtmlHelper;
import org.glassfish.grizzly.http.server.util.MappingData;
import org.glassfish.grizzly.http.util.CharChunk;
import org.glassfish.grizzly.http.util.Header;
import org.glassfish.grizzly.http.util.HttpRequestURIDecoder;
import org.glassfish.grizzly.http.util.HttpStatus;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;

/**
 * HttpHandler implementation that provides an entry point for processing a Servlet request.
 *
 * @author Jeanfrancois Arcand
 */
public class ServletHandler extends HttpHandler {

    private static final Logger LOGGER = Grizzly.logger(ServletHandler.class);

    static final Note<HttpServletRequestImpl> SERVLET_REQUEST_NOTE = Request.createNote(HttpServletRequestImpl.class.getName());
    static final Note<HttpServletResponseImpl> SERVLET_RESPONSE_NOTE = Request.createNote(HttpServletResponseImpl.class.getName());

    static final ServletAfterServiceListener servletAfterServiceListener = new ServletAfterServiceListener();

    protected String servletClassName;
    protected Class<? extends Servlet> servletClass;
    protected volatile Servlet servletInstance = null;
    private String contextPath = "";
    private final Object lock = new Object();

    /**
     * The {@link WebappContext}
     */
    private final WebappContext servletCtx;
    /**
     * The {@link ServletConfigImpl}
     */
    private final ServletConfigImpl servletConfig;
    /**
     * The {@link SessionManager}
     */
    private SessionManager sessionManager = ServletSessionManager.instance();
    /**
     * Holder for our configured properties.
     */
    protected final Map<String, Object> properties = new HashMap<>();
    /**
     * Initialize the {@link ServletContext}
     */
    protected boolean initialize = true;
    protected ClassLoader classLoader;

    protected ExpectationHandler expectationHandler;

    protected FilterChainFactory filterChainFactory;

    // Listeners to be invoked when ServletHandler.destroy is called
    private List<Runnable> onDestroyListeners;

    // ------------------------------------------------------------ Constructors

    protected ServletHandler(final ServletConfigImpl servletConfig) {
        this.servletConfig = servletConfig;
        servletCtx = (WebappContext) servletConfig.getServletContext();
    }

    // ---------------------------------------------------------- Public Methods

    /**
     * {@inheritDoc}
     */
    @Override
    public void start() {
        try {
            configureServletEnv();
        } catch (Throwable t) {
            LOGGER.log(Level.SEVERE, "start", t);
        }
    }

    /**
     * Override parent's
     * {@link HttpHandler#sendAcknowledgment(org.glassfish.grizzly.http.server.Request, org.glassfish.grizzly.http.server.Response)}
     * to let {@link ExpectationHandler} (if one is registered) process the expectation.
     */
    @Override
    protected boolean sendAcknowledgment(Request request, Response response) throws IOException {
        return expectationHandler != null || super.sendAcknowledgment(request, response);

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void service(Request request, Response response) throws Exception {
        if (classLoader != null) {
            final ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(classLoader);
            try {
                doServletService(request, response);
            } finally {
                Thread.currentThread().setContextClassLoader(prevClassLoader);
            }
        } else {
            doServletService(request, response);
        }
    }

    protected void doServletService(final Request request, final Response response) {
        try {
            final String uri = request.getRequestURI();

            // The request is not for us.
            if (contextPath.length() > 0 && !uri.startsWith(contextPath)) {
                customizeErrorPage(response, "Resource Not Found", 404, null);
                return;
            }

            final HttpServletRequestImpl servletRequest = HttpServletRequestImpl.create();
            final HttpServletResponseImpl servletResponse = HttpServletResponseImpl.create();

            setPathData(request, servletRequest);
            servletRequest.initialize(request, servletResponse, servletCtx);
            servletResponse.initialize(response, servletRequest);

            request.setNote(SERVLET_REQUEST_NOTE, servletRequest);
            request.setNote(SERVLET_RESPONSE_NOTE, servletResponse);

            request.addAfterServiceListener(servletAfterServiceListener);

            loadServlet();

            setDispatcherPath(request, getCombinedPath(servletRequest));

            final String serverInfo = servletCtx.getServerInfo();
            if (serverInfo != null && !serverInfo.isEmpty()) {
                servletResponse.addHeader(Header.Server.toString(), serverInfo);
            }

            if (expectationHandler != null) {
                final AckActionImpl ackAction = new AckActionImpl(response);
                expectationHandler.onExpectAcknowledgement(servletRequest, servletResponse, ackAction);
                if (!ackAction.isAcknowledged()) {
                    ackAction.acknowledge();
                } else if (ackAction.isFailAcknowledgement()) {
                    return;
                }
            }

            FilterChainInvoker filterChain = getFilterChain(request);
            if (filterChain != null) {
                filterChain.invokeFilterChain(servletRequest, servletResponse);
            } else {
                servletInstance.service(servletRequest, servletResponse);
            }

            // Request may want to initialize async processing
            servletRequest.onAfterService();
        } catch (Throwable ex) {
            LOGGER.log(Level.SEVERE, "service exception:", ex);
            customizeErrorPage(response, "Internal Error", 500, ex);
        }
    }

    protected FilterChainInvoker getFilterChain(Request request) {
        if (filterChainFactory != null) {
            return filterChainFactory.createFilterChain(request, servletInstance, REQUEST);
        }
        return null;
    }

    private void setDispatcherPath(final Request request, final String path) {
        request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, path);
    }

    /**
     * Combines the servletPath and the pathInfo.
     *
     * If pathInfo is <code>null</code>, it is ignored. If servletPath is <code>null</code>, then <code>null</code> is
     * returned.
     *
     * @return The combined path with pathInfo appended to servletInfo
     */
    private String getCombinedPath(final HttpServletRequest request) {
        if (request.getServletPath() == null) {
            return null;
        }
        if (request.getPathInfo() == null) {
            return request.getServletPath();
        }
        return request.getServletPath() + request.getPathInfo();
    }

    protected void setPathData(final Request from, final HttpServletRequestImpl to) {

        final MappingData data = from.obtainMappingData();
        to.setServletPath(data.wrapperPath.toString());
        to.setPathInfo(data.pathInfo.toString());
        to.setContextPath(data.contextPath.toString());
    }

    void doServletService(final ServletRequest servletRequest, final ServletResponse servletResponse, final DispatcherType dispatcherType)
            throws IOException, ServletException {
        try {
            loadServlet();
            FilterChainImpl filterChain = filterChainFactory.createFilterChain(servletRequest, servletInstance, dispatcherType);
            if (filterChain != null) {
                filterChain.invokeFilterChain(servletRequest, servletResponse);
            } else {
                servletInstance.service(servletRequest, servletResponse);
            }
        } catch (ServletException se) {
            LOGGER.log(Level.SEVERE, "service exception:", se);
            throw se;
        } catch (IOException ie) {
            LOGGER.log(Level.SEVERE, "service exception:", ie);
            throw ie;
        }
    }

    /**
     * Customize the error page returned to the client.
     * 
     * @param response the {@link Response}
     * @param message the HTTP error message
     * @param errorCode the error code.
     */
    public void customizeErrorPage(final Response response, final String message, final int errorCode, final Throwable t) {
        if (!response.isCommitted()) {
            try {
                HtmlHelper.setErrorAndSendErrorPage(response.getRequest(), response, response.getErrorPageGenerator(), errorCode, message, message, t);
            } catch (IOException ex) {
                // We are in a very bad shape. Ignore.
            }
        }
    }

    /**
     * Load a {@link Servlet} instance.
     *
     * @throws jakarta.servlet.ServletException If failed to {@link Servlet#init(jakarta.servlet.ServletConfig)}.
     */
    protected void loadServlet() throws ServletException {

        if (servletInstance == null) {
            synchronized (lock) {
                if (servletInstance == null) {
                    Servlet newServletInstance;
                    if (servletClassName != null) {
                        newServletInstance = (Servlet) ClassLoaderUtil.load(servletClassName);
                    } else {
                        try {
                            newServletInstance = servletClass.newInstance();
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    LOGGER.log(Level.INFO, "Loading Servlet: {0}", newServletInstance.getClass().getName());
                    newServletInstance.init(servletConfig);
                    servletInstance = newServletInstance;
                }
            }
        }

    }

    /**
     * Configure the {@link WebappContext} and {@link ServletConfigImpl}
     *
     * @throws jakarta.servlet.ServletException Error while configuring {@link Servlet}.
     */
    protected void configureServletEnv() throws ServletException {
        if (contextPath.length() > 0) {
            final CharChunk cc = new CharChunk();
            char[] ch = contextPath.toCharArray();
            cc.setChars(ch, 0, ch.length);
            HttpRequestURIDecoder.normalizeChars(cc);
            contextPath = cc.toString();
        }

        if ("".equals(contextPath)) {
            contextPath = "";
        }

    }

    /**
     * Return the {@link Servlet} instance used by this {@link ServletHandler}
     * 
     * @return {@link Servlet} instance.
     */
    @SuppressWarnings({ "UnusedDeclaration" })
    public Servlet getServletInstance() {
        return servletInstance;
    }

    /**
     * Set the {@link Servlet} instance used by this {@link ServletHandler}
     * 
     * @param servletInstance an instance of Servlet.
     */
    protected void setServletInstance(Servlet servletInstance) {
        this.servletInstance = servletInstance;
    }

    protected void setServletClassName(final String servletClassName) {
        this.servletClassName = servletClassName;
    }

    protected void setServletClass(final Class<? extends Servlet> servletClass) {
        this.servletClass = servletClass;
    }

    /**
     * Set the {@link SessionManager} instance used by this {@link ServletHandler}
     * 
     * @param sessionManager an implementation of SessionManager.
     */
    protected void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    /**
     *
     * Returns the portion of the request URI that indicates the context of the request. The context path always comes first
     * in a request URI. The path starts with a "/" character but does not end with a "/" character. For servlets in the
     * default (root) context, this method returns "". The container does not decode this string.
     *
     * <p>
     * It is possible that a servlet container may match a context by more than one context path. In such cases this method
     * will return the actual context path used by the request and it may differ from the path returned by the
     * {@link jakarta.servlet.ServletContext#getContextPath()} method. The context path returned by
     * {@link jakarta.servlet.ServletContext#getContextPath()} should be considered as the prime or preferred context path
     * of the application.
     *
     * @return a <code>String</code> specifying the portion of the request URI that indicates the context of the request
     *
     * @see jakarta.servlet.ServletContext#getContextPath()
     */
    public String getContextPath() {
        return contextPath;
    }

    /**
     * Programmatically set the context path of the Servlet.
     *
     * @param contextPath Context path.
     */
    public void setContextPath(String contextPath) {
        this.contextPath = contextPath;
    }

    /**
     * Destroy this Servlet and its associated {@link jakarta.servlet.ServletContextListener}
     */
    @Override
    public void destroy() {
        try {
            if (classLoader != null) {
                ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(classLoader);
                try {
                    super.destroy();

                    if (servletInstance != null) {
                        servletInstance.destroy();
                        servletInstance = null;
                    }
                } finally {
                    Thread.currentThread().setContextClassLoader(prevClassLoader);
                }
            } else {
                super.destroy();
            }
        } finally {
            // notify destroy listeners (if any)
            if (onDestroyListeners != null) {
                for (int i = 0; i < onDestroyListeners.size(); i++) {
                    try {
                        onDestroyListeners.get(i).run();
                    } catch (Throwable t) {
                        LOGGER.log(Level.WARNING, "onDestroyListener error", t);
                    }
                }

                onDestroyListeners = null;
            }
        }
    }

    protected WebappContext getServletCtx() {
        return servletCtx;
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public ServletConfigImpl getServletConfig() {
        return servletConfig;
    }

    @Override
    public String getName() {
        return servletConfig.getServletName();
    }

    /**
     * Get the {@link ExpectationHandler} responsible for processing <tt>Expect:</tt> header (for example "Expect:
     * 100-Continue").
     *
     * @return the {@link ExpectationHandler} responsible for processing <tt>Expect:</tt> header (for example "Expect:
     * 100-Continue").
     */
    public ExpectationHandler getExpectationHandler() {
        return expectationHandler;
    }

    /**
     * Set the {@link ExpectationHandler} responsible for processing <tt>Expect:</tt> header (for example "Expect:
     * 100-Continue").
     *
     * @param expectationHandler the {@link ExpectationHandler} responsible for processing <tt>Expect:</tt> header (for
     * example "Expect: 100-Continue").
     */
    public void setExpectationHandler(ExpectationHandler expectationHandler) {
        this.expectationHandler = expectationHandler;
    }

    @Override
    protected void setDispatcherHelper(final DispatcherHelper dispatcherHelper) {
        servletCtx.setDispatcherHelper(dispatcherHelper);
    }

    protected void setFilterChainFactory(final FilterChainFactory filterChainFactory) {
        this.filterChainFactory = filterChainFactory;
    }

    /**
     * Overrides default (JSESSIONID) session cookie name.
     * 
     * @return the session cookie name
     */
    @Override
    protected String getSessionCookieName() {
        return servletCtx.getSessionCookieConfig().getName();
    }

    /**
     * @return Servlet-aware {@link SessionManager}
     */
    @Override
    protected SessionManager getSessionManager(Request request) {
        final SessionManager sm = request.getHttpFilter().getConfiguration().getSessionManager();
        return sm != null ? sm : this.sessionManager;
    }

    /**
     * Adds the {@link Runnable}, which will be invoked when {@link #destroy()} is called.
     * 
     * @param r {@link Runnable}, which contains destroy listener logic.
     */
    void addOnDestroyListener(final Runnable r) {
        if (onDestroyListeners == null) {
            onDestroyListeners = new ArrayList<>(2);
        }

        onDestroyListeners.add(r);
    }

    // ---------------------------------------------------------- Nested Classes

    /**
     * AfterServiceListener, which is responsible for recycle servlet request and response objects.
     */
    static final class ServletAfterServiceListener implements AfterServiceListener {

        @Override
        public void onAfterService(final Request request) {
            final HttpServletRequestImpl servletRequest = getServletRequest(request);
            final HttpServletResponseImpl servletResponse = getServletResponse(request);

            if (servletRequest != null) {
                servletRequest.recycle();
                servletResponse.recycle();
            }
        }
    }

    static final class AckActionImpl implements ExpectationHandler.AckAction {
        private boolean isAcknowledged;
        private boolean isFailAcknowledgement;

        private final Response response;

        private AckActionImpl(final Response response) {
            this.response = response;
        }

        @Override
        public void acknowledge() throws IOException {
            if (isAcknowledged) {
                throw new IllegalStateException("Already acknowledged");
            }

            isAcknowledged = true;
            response.setStatus(HttpStatus.CONINTUE_100);
            response.sendAcknowledgement();
        }

        @Override
        public void fail() throws IOException {
            if (isAcknowledged) {
                throw new IllegalStateException("Already acknowledged");
            }
            isAcknowledged = true;
            isFailAcknowledgement = true;

            response.setStatus(HttpStatus.EXPECTATION_FAILED_417);
            response.finish();
        }

        public boolean isAcknowledged() {
            return isAcknowledged;
        }

        public boolean isFailAcknowledgement() {
            return isFailAcknowledgement;
        }
    }

    static HttpServletRequestImpl getServletRequest(final Request request) {
        return request.getNote(SERVLET_REQUEST_NOTE);
    }

    static HttpServletResponseImpl getServletResponse(final Request request) {
        return request.getNote(SERVLET_RESPONSE_NOTE);
    }
}
